package com.breeze.components.task.lock;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.mybatisflex.core.row.Db;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;

/**
 * 锁处理
 *
 * @author breeze
 */
@Slf4j
public class DistributedLockService {

  public <T> T tryLockRet(String lockKey, Supplier<T> supplier) {
    return tryLockRet(lockKey, null, ScheduleType.NONE, null, supplier);
  }

  public <T> T tryLockRet(
      String lockKey,
      String methodName,
      ScheduleType scheduleType,
      String scheduleConf,
      Supplier<T> supplier) {
    return Db.txWithResult(
        () -> {
          Lock reentrantLock = new ReentrantLock();
          try {
            reentrantLock.lock();
            // 不存在新建锁
            if (!existLock(lockKey)) {
              try {
                insertLock(lockKey, methodName, scheduleType, scheduleConf);
              } catch (DuplicateKeyException ignore) {
                // 已存在直接抢占锁
              }
            }
            // 抢占锁,获取下次调度时间
            DistributedLockDO lock = tryLock(lockKey);
            if (Objects.isNull(lock)) {
              return null;
            }

            // 抢到锁获取当前时间
            Date now = new Date();

            // 当前时间小于下次调度时间,不执行调度方法
            if (now.before(Optional.ofNullable(lock.getTriggerNextTime()).orElse(DateTime.now()))) {
              return null;
            }

            // 执行业务
            T result = supplier.get();

            // 更新任务下次调度时间
            try {
              updateLock(
                  lockKey,
                  methodName,
                  scheduleType,
                  scheduleConf,
                  LockScheduledUtil.generateNextValidTime(
                      scheduleType,
                      scheduleConf,
                      Objects.equals(ScheduleType.FIXED_DELAY, scheduleType) ? new Date() : now));
              return result;
            } catch (Exception e) {
              throw new RuntimeException(e);
            }
          } finally {
            reentrantLock.unlock();
          }
        });
  }

  private DistributedLockDO tryLock(String lockKey) {
    return Db.selectOneBySql(
            "select * from `distributed_lock` where `lock_key` = ? for update", lockKey)
        .toObject(DistributedLockDO.class);
  }

  private boolean existLock(String lockKey) {
    return Db.selectCount("select COUNT(1) from `distributed_lock` where `lock_key` = ?", lockKey)
        > 0;
  }

  private void insertLock(
      String lockKey, String methodName, ScheduleType scheduleType, String scheduleConf) {
    Db.insertBySql(
        "insert ignore into `distributed_lock`(`lock_key`, `method_name`, `schedule_type`, `schedule_conf`) values (?, ?, ?, ?)",
        lockKey,
        methodName,
        scheduleType.name(),
        scheduleConf);
  }

  private void updateLock(
      String lockKey,
      String methodName,
      ScheduleType scheduleType,
      String scheduleConf,
      Date triggerNextTime) {
    Db.updateBySql(
        "update `distributed_lock` set `method_name` = ?, `schedule_type` = ?, `schedule_conf` = ?, `trigger_next_time` = ? where `lock_key` = ?",
        methodName,
        scheduleType.name(),
        scheduleConf,
        DateUtil.format(triggerNextTime, DatePattern.NORM_DATETIME_PATTERN),
        lockKey);
  }
}
